package org.movee.spring.data.hugegraph.repository.query;

import org.movee.spring.data.hugegraph.query.paramerter.HugeParameterAccessor;
import org.movee.spring.data.hugegraph.query.paramerter.HugeParameters;
import lombok.extern.slf4j.Slf4j;
import org.springframework.data.repository.query.Parameter;
import org.springframework.data.repository.query.QueryMethodEvaluationContextProvider;
import org.springframework.expression.EvaluationContext;
import org.springframework.expression.Expression;
import org.springframework.expression.spel.standard.SpelExpressionParser;
import org.springframework.util.Assert;
import org.springframework.util.CollectionUtils;

import javax.annotation.Nullable;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;

@Slf4j
class ExpressionEvaluatingParameterBinder {

    private final SpelExpressionParser expressionParser;
    private final QueryMethodEvaluationContextProvider evaluationContextProvider;

    ExpressionEvaluatingParameterBinder(SpelExpressionParser expressionParser,
                                        QueryMethodEvaluationContextProvider evaluationContextProvider) {

        Assert.notNull(expressionParser, "ExpressionParser must not be null");
        Assert.notNull(evaluationContextProvider, "evaluationContextProvider must not be null");

        this.expressionParser = expressionParser;
        this.evaluationContextProvider = evaluationContextProvider;
    }

    public List<Object> bind(HugeParameterAccessor parameterAccessor, BindingContext bindingContext) {

        if (!bindingContext.hasBindings()) {
            return Collections.emptyList();
        }

        List<Object> parameters = new ArrayList<>(bindingContext.getBindings().size());

        bindingContext.getBindings() //
                .stream() //
                .map(binding -> getParameterValueForBinding(parameterAccessor, bindingContext.getParameters(), binding)) //
                .forEach(parameters::add);

        return parameters;
    }

    private Object getParameterValueForBinding(HugeParameterAccessor parameterAccessor,
                                               HugeParameters parameters, ParameterBinding binding) {

        if (binding.isExpression()) {
            return evaluateExpression(binding.getExpression(), parameters, parameterAccessor.getValues());
        }

        return binding.isNamed()
                ? parameterAccessor.getBindableValue(getParameterIndex(parameters, binding.getParameterName()))
                : parameterAccessor.getBindableValue(binding.getParameterIndex());
    }

    private int getParameterIndex(HugeParameters parameters, String parameterName) {

        return parameters.stream() //
                .filter(gremlinParameter -> gremlinParameter //
                        .getName().filter(s -> s.equals(parameterName)) //
                        .isPresent()) //
                .mapToInt(Parameter::getIndex) //
                .findFirst() //
                .orElseThrow(() -> new IllegalArgumentException(
                        String.format("Invalid parameter name; Cannot resolve parameter [%s]", parameterName)));

    }

    /**
     * Evaluates the given {@code expressionString}.
     *
     * @param expressionString must not be {@literal null} or empty.
     * @param parameters must not be {@literal null}.
     * @param parameterValues must not be {@literal null}.
     * @return the value of the {@code expressionString} evaluation.
     */
    private Object evaluateExpression(String expressionString, HugeParameters parameters, Object[] parameterValues) {

        EvaluationContext evaluationContext = evaluationContextProvider.getEvaluationContext(parameters, parameterValues);
        Expression expression = expressionParser.parseExpression(expressionString);

        return expression.getValue(evaluationContext, Object.class);
    }

    /**
     * @author Mark Paluch
     * @since 1.5
     */
    static class BindingContext {

        final HugeQueryMethod queryMethod;
        final List<ParameterBinding> bindings;

        public BindingContext(HugeQueryMethod queryMethod, List<ParameterBinding> bindings) {

            this.queryMethod = queryMethod;
            this.bindings = bindings;
        }


        boolean hasBindings() {
            return !CollectionUtils.isEmpty(bindings);
        }

        List<ParameterBinding> getBindings() {
            return Collections.unmodifiableList(bindings);
        }


        HugeParameters getParameters() {
            return queryMethod.getParameters();
        }


        HugeQueryMethod getQueryMethod() {
            return queryMethod;
        }
    }

    static class ParameterBinding {

        private final boolean quoted;
        private final int parameterIndex;
        private final @Nullable String expression;
        private final @Nullable String parameterName;

        private ParameterBinding(int parameterIndex, boolean quoted, @Nullable String expression,
                                 @Nullable String parameterName) {

            this.parameterIndex = parameterIndex;
            this.quoted = quoted;
            this.expression = expression;
            this.parameterName = parameterName;
        }

        static ParameterBinding expression(String expression, boolean quoted) {
            return new ParameterBinding(-1, quoted, expression, null);
        }

        static ParameterBinding indexed(int parameterIndex) {
            return new ParameterBinding(parameterIndex, false, null, null);
        }

        static ParameterBinding named(String name) {
            return new ParameterBinding(-1, false, null, name);
        }

        boolean isNamed() {
            return (parameterName != null);
        }

        int getParameterIndex() {
            return parameterIndex;
        }

        String getParameter() {
            return ("?" + (isExpression() ? "expr" : "") + parameterIndex);
        }

        @Nullable
        String getExpression() {
            return expression;
        }

        boolean isExpression() {
            return (this.expression != null);
        }

        @Nullable
        String getParameterName() {
            return parameterName;
        }
    }
}

